﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using RoutingDaemon.Entities;
using RoutingDaemon.ServerInterface.Commands;
using RoutingDaemon.Utilities;
using IRC.Utilities;
using RoutingDaemon.Backend;
using IRC.Utilities.Entities;

namespace RoutingDaemon
{
    public class RoutingDaemon
    {
        private const int MAXMESSAGESIZE = 1024;
        private const int Second = 1000;
        private const int AdvertisementCycleTime = 30 * Second;
        private const int NeighborTimeout = 120 * Second;
        private const int RetransmissionTimeout = 3 * Second;
        private Socket daemonTCPSocket;
        private Socket daemonUDPSocket;
        private IRCServer ircServer;
        private List<Thread> threads;
        private bool silent = false;

        public RoutingDaemon(int localNodeID, List<IRC.Utilities.Entities.NodeConfiguration> configuation)
        {
            DaemonBackEnd.Reset();
            // Set the local node ID.
            DaemonBackEnd.Instance.LocalNode.NodeID = localNodeID;

            //Logger.Instance.Debug("Loaded configuration file");

            // Configure all the neighbors of the local node
            DaemonBackEnd.Instance.ConfigureLocalNode(configuation);

            this.StartDaemon(
                 DaemonBackEnd.Instance.LocalNode.Configuration.RoutingPort,
                 DaemonBackEnd.Instance.LocalNode.Configuration.LocalPort
                 );

            //Logger.Instance.Info("Daemon Started Successfully");


        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RoutingDaemon"/> class.
        /// </summary>
        /// <param name="routingPort">The UDP port on the routing daemon used to exchange routing information with other routing daemons.</param>
        /// <param name="localPort">The TCP port on the routing daemon that is used to exchange information between it and the local IRC server.</param>
        private void StartDaemon(int routingPort, int localPort)
        {
            try
            {
                // Initialize the socket that will wait for commands from the local IRC server.
                this.daemonTCPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                this.daemonTCPSocket.Bind(new IPEndPoint(IPAddress.Any, localPort));

                //Logger.Instance.Warn(string.Format("Listening to IRC Server on port {0}", localPort));
                // Initialize the socket that will communicate with other routing daemon nodes.
                this.daemonUDPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                this.daemonUDPSocket.Bind(new IPEndPoint(IPAddress.Any, routingPort));
                //Logger.Instance.Warn(string.Format("Listening to Neighbor Daemons on port {0}", routingPort));
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }

        }

        public void Silence()
        {
            foreach (Thread thread in threads)
            {
                thread.Suspend();
            }

            this.silent = true;

            foreach (Thread thread in threads)
            {
                thread.Resume();
            }
        }

        public void Unsilence()
        {
            this.silent = false;
        }

        public void Shutdown()
        {
            this.daemonTCPSocket.Close();
            this.daemonUDPSocket.Close();

            foreach(Thread thread in threads)
            {
                thread.Abort();
            }

        }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
            this.daemonTCPSocket.Listen(10);

            threads = new List<Thread>();
            // Start a thread for listening on TCP port
            Thread serverConnectionThread = new Thread(new ThreadStart(this.WaitForIRCServerConnection));
            serverConnectionThread.Start();
            threads.Add(serverConnectionThread);

            // Start a thread waiting to receive LSAs
            Thread receiveLSAThread = new Thread(new ThreadStart(this.WaitForLSAs));
            receiveLSAThread.Start();
            threads.Add(receiveLSAThread);

            // Start a thread that sends LSAs every 30 seconds
            Thread broadcastLSAsThread = new Thread(new ThreadStart(this.BroadcastLSA));
            broadcastLSAsThread.Start();
            threads.Add(broadcastLSAsThread);

            // Start a thread that checks for down neighbor
            Thread neighborDownTimeoutThread = new Thread(new ThreadStart(this.MarkNeighborAsDown));
            neighborDownTimeoutThread.Start();
            threads.Add(neighborDownTimeoutThread);
            // Infinite Loop till the application exits.

            while (true)
            {
                System.Threading.Thread.Sleep(100000);
            }
        }

        /// <summary>
        /// Waits for IRC server connections.
        /// </summary>
        private void WaitForIRCServerConnection()
        {
            try
            {
                // Will only communicate with only 1 IRC server.
                this.ircServer = new IRCServer(this.daemonTCPSocket);
                while (true)
                {
                    // Wait for commands from the IRC server
                    string response = String.Empty;
                    try
                    {
                        DaemonCommandBase command = this.ircServer.ReceiveCommand();
                        if (command == null)
                        {
                            //Logger.Instance.Debug("Connection to IRC server terminated.");
                            this.ircServer = new IRCServer(this.daemonTCPSocket);
                        }
                        else
                        {
                            response = command.ExecuteCommand();
                            ircServer.SendResponse(response);
                            //Logger.Instance.Info(response);
                        }
                        //Logger.Instance.Warn(string.Format("Command response: {0}", response));
                    }
                    catch
                    {
                        response = Errors.GetErrorResponse(ErrorCode.ERR_UNKNOWNCOMMAND, String.Empty);
                    }
                }
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }
        }

        /// <summary>
        /// Waits for routing daemon connections.
        /// </summary>
        private void WaitForLSAs()
        {
            try
            {
                byte[] buffer;
                int messageLength;
                IPEndPoint endpoint;
                Node sourceNeighbor;
                EndPoint remoteDaemonEndpoint = new IPEndPoint(IPAddress.Any, 0);
                while (true)
                {
                    buffer = new byte[MAXMESSAGESIZE];

                    try
                    {
                        messageLength = this.daemonUDPSocket.ReceiveFrom(buffer, ref remoteDaemonEndpoint);
                    }
                    catch
                    {
                        continue;
                    }

                    try
                    {
                        int sourceNeighborID = Backend.DaemonBackEnd.Instance.Configuration.First(c =>
                            c.GetNodeEndPoint().Equals((IPEndPoint)remoteDaemonEndpoint)).NodeID;
                        sourceNeighbor = Backend.DaemonBackEnd.Instance.GetNodeByID(sourceNeighborID);
                    }
                    catch
                    {
                        //Logger.Instance.Error("[WaitForLSAs] Failed to find source of message");
                        continue;
                    }

                    byte[] newBuffer = new byte[messageLength];
                    Array.Copy(buffer, 0, newBuffer, 0, messageLength);
                    LSA receivedLSA = LSAUtility.CreateLSAFromByteArray(newBuffer);
                    Node node = Backend.DaemonBackEnd.Instance.GetNodeByID(receivedLSA.SenderNodeID);
                    if (node == null)
                    {
                        //Logger.Instance.Debug(string.Format("[WaitForLSAs] Node {0} Missing", receivedLSA.SenderNodeID));
                        continue;
                    }

                    if (node.IsDown)
                    {
                        //Logger.Instance.Warn(string.Format("[WaitForLSAs] Node {0} is back up!", receivedLSA.SenderNodeID));
                    }

                    node.IsDown = false;
                    node.LastUpdateTime = DateTime.Now;

                    if (receivedLSA.Type == LSAType.Acknowledgement)
                    {
                        node.IsAcknowledged = true;
                        //Logger.Instance.Info(string.Format("[WaitForLSAs] Received Acknowledgment from Node: {0}", receivedLSA.SenderNodeID));
                        continue;
                    }

                    LSA correctedLSA = Backend.DaemonBackEnd.Instance.UpdateBackEndWithLSA(receivedLSA);
                    if (correctedLSA != null)
                    {
                        //Logger.Instance.Info(string.Format("[WaitForLSAs] Received Out Of Sequence LSA from Node: {0}", receivedLSA.SenderNodeID));

                        if (receivedLSA.SequenceNumber == correctedLSA.SequenceNumber)
                        {
                            //Logger.Instance.Info(string.Format("[WaitForLSAs] LSA of Node {0} is duplicate. Ignoring it.", receivedLSA.SenderNodeID));
                            continue;
                        }

                        newBuffer = LSAUtility.GetByteArrayFromLSA(correctedLSA);

                        if (!silent)
                            this.daemonUDPSocket.SendTo(newBuffer, remoteDaemonEndpoint);
                        //Logger.Instance.Info(string.Format("[WaitForLSAs] Sent Corrected LSA to Node: {0}", receivedLSA.SenderNodeID));
                    }
                    else
                    {
                        if (receivedLSA.SenderNodeID == Backend.DaemonBackEnd.Instance.LocalNode.NodeID)
                        {//Logger.Instance.Info(string.Format("[WaitForLSAs] Received corrected LSA via Node: {0}", sourceNeighbor.NodeID));
                        }
                        else
                            //Logger.Instance.Info(string.Format("[WaitForLSAs] Received LSA from Node: {0} via Node: {1}", receivedLSA.SenderNodeID, sourceNeighbor.NodeID));
                            // Send ACK LSA to the sender.
                            newBuffer = LSAUtility.CreateAckLSAFromLSA(receivedLSA);

                        if (sourceNeighbor.NodeID == receivedLSA.SenderNodeID)
                        {
                            if (!silent)
                                this.daemonUDPSocket.SendTo(newBuffer, remoteDaemonEndpoint);
                            //Logger.Instance.Info(string.Format("[WaitForLSAs] Sent ACK LSA to Node: {0}", sourceNeighbor.NodeID));
                        }

                        // flood the new announcement to all of its neighbors except the one from which the announcement was received, and will then update its own routing tables.
                        foreach (Node neighborNode in Backend.DaemonBackEnd.Instance.LocalNode.Neighbors.Where(n => !n.IsDown).ToList())
                        {
                            if (neighborNode.NodeID == sourceNeighbor.NodeID)
                            {
                                continue;
                            }

                            endpoint = null;
                            try
                            {
                                endpoint = neighborNode.Configuration.GetNodeEndPoint();
                            }
                            catch
                            {
                                //Logger.Instance.Error(string.Format("[WaitForLSAs] Unable to flood LSA, invalid Hostname/port combination {0}: {1}", neighborNode.Configuration.Hostname, neighborNode.Configuration.RoutingPort));
                            }

                            if (endpoint != null)
                            {
                                if (!silent)
                                    this.daemonUDPSocket.SendTo(LSAUtility.GetByteArrayFromLSA(receivedLSA), endpoint);
                                //Logger.Instance.Info(string.Format("[WaitForLSAs] Flooding LSA from Node {0} to Node: {1}", receivedLSA.SenderNodeID, neighborNode.NodeID));
                            }
                        }
                    }
                }
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }
        }

        /// <summary>
        /// Broadcasts the local LSA to all neighbors.
        /// </summary>
        private void BroadcastLSA()
        {
            try
            {
                while (true)
                {
                    //Logger.Instance.Debug("[BroadcastLSA] Initializing...");
                    Backend.DaemonBackEnd.Instance.LocalNode.LastSequenceNumber++;
                    LSA localNodeLSA = Backend.DaemonBackEnd.Instance.GetLocalNodeLSA();
                    byte[] localLSAbuffer = LSAUtility.GetByteArrayFromLSA(localNodeLSA);
                    foreach (Node node in Backend.DaemonBackEnd.Instance.LocalNode.Neighbors.Where(n => !n.IsDown).ToList())
                    {
                        node.IsAcknowledged = false;
                        IPEndPoint endpoint = null;
                        try
                        {
                            endpoint = node.Configuration.GetNodeEndPoint();
                            //Logger.Instance.Debug(string.Format("[BroadcastLSA] LSA sent to {0}:{1}", node.Configuration.Hostname, node.Configuration.RoutingPort));
                        }
                        catch
                        {
                            //Logger.Instance.Error(string.Format("[BroadcastLSA] Unable to broadcast LSA, invalid Hostname/port combination {0}: {1}", node.Configuration.Hostname, node.Configuration.RoutingPort));
                        }

                        if (endpoint != null)
                        {
                            if (!silent)
                                this.daemonUDPSocket.SendTo(localLSAbuffer, endpoint);
                        }
                    }

                    Thread waitForAckThread = new Thread(new ThreadStart(this.WaitForAck));
                    waitForAckThread.Start();
                    //Logger.Instance.Debug("[BroadcastLSA] Finished.");
                    Thread.Sleep(AdvertisementCycleTime);
                }
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }
        }

        private void WaitForAckOfFloodedLSA(LSA lsa)
        {

        }

        /// <summary>
        /// Waits for acknowledgment LSA from all neighbors.
        /// </summary>
        private void WaitForAck()
        {
            try
            {
                int currentSequenceNumber = Backend.DaemonBackEnd.Instance.LocalNode.LastSequenceNumber;
                bool allAck = true;
                while (true)
                {
                    if (currentSequenceNumber != Backend.DaemonBackEnd.Instance.LocalNode.LastSequenceNumber)
                    {
                        break;
                    }

                    Thread.Sleep(RetransmissionTimeout);
                    LSA localNodeLSA = Backend.DaemonBackEnd.Instance.GetLocalNodeLSA();
                    byte[] localLSAbuffer = LSAUtility.GetByteArrayFromLSA(localNodeLSA);

                    allAck = true;

                    //Logger.Instance.Info(string.Format("[WaitForAck] Checking if all up neighbors have sent ACKs"));

                    foreach (Node node in Backend.DaemonBackEnd.Instance.LocalNode.Neighbors.Where(n => !n.IsDown).ToList())
                    {
                        if (!node.IsAcknowledged)
                        {
                            IPEndPoint endpoint = null;
                            try
                            {
                                endpoint = node.Configuration.GetNodeEndPoint();
                            }
                            catch
                            {
                                //Logger.Instance.Error(string.Format("[WaitForAck] Unable to re-transmit LSA, invalid Hostname/port combination {0}: {1}", node.Configuration.Hostname, node.Configuration.RoutingPort));
                            }

                            if (endpoint != null)
                            {
                                if (!silent)
                                    this.daemonUDPSocket.SendTo(localLSAbuffer, endpoint);
                                //Logger.Instance.Info(string.Format("[WaitForAck] Retransmitting LSA to Node: {0}", node.NodeID));
                            }

                            allAck = false;
                        }
                    }

                    if (allAck)
                    {
                        break;
                    }
                }
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }
        }

        /// <summary>
        /// Marks the neighbor as down.
        /// </summary>
        private void MarkNeighborAsDown()
        {
            try
            {
                while (true)
                {
                    Thread.Sleep(NeighborTimeout);
                    //Logger.Instance.Debug("[MarkNeighborAsDown] check initialized");
                    foreach (Node node in Backend.DaemonBackEnd.Instance.LocalNode.Neighbors.Where(n => !n.IsDown).ToList())
                    {
                        if (DateTime.Now - node.LastUpdateTime >= new TimeSpan(0, 0, NeighborTimeout / Second))
                        {
                            node.IsDown = true;
                            //Logger.Instance.Error(string.Format("[MarkNeighborAsDown] Marking Node {0} as down", node.NodeID));
                        }
                    }
                    //Logger.Instance.Debug("[MarkNeighborAsDown] check finished.");
                }
            }
            catch
            {
                Thread.CurrentThread.Abort();
            }
        }


    }
}
